package org.dicr.gui;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.lang.reflect.*;

import javax.swing.*;

/***********************************************************************************************************************
 * This class contain small but useful functions. This class is not meant to be subclassed.
 * 
 * @author Igor A Tarasov me@dicr.dn.ua
 * @version 020913
 **********************************************************************************************************************/
public final class GuiMisc {
	public static final Cursor CURSOR_WAIT = new Cursor(Cursor.WAIT_CURSOR);

	public static final Cursor CURSOR_DEFAULT = new Cursor(Cursor.DEFAULT_CURSOR);

	/** Frame used to create dummyWindow */
	private static Frame dummyFrame = null;

	/** Window used to create images */
	private static Window dummyWindow = null;

	/** Offset to the calling method in a stack trace */
	private static int levelOffset = GuiMisc.calculateLevelOffset();

	/*******************************************************************************************************************
	 * Расчитывает размеры компонента, при которых он помещается в контейнер. При необходимости высота или ширина
	 * обрезаются до размера контейнера.
	 * 
	 * @param component размеры компонента
	 * @param container размеры контейнера
	 * @return урезанные размеры компонента, не больше размеров контейнера.
	 ******************************************************************************************************************/
	public static Dimension calcCropFitDimension(final Dimension component, final Dimension container) {
		final Dimension cdim = component.getSize();
		if (cdim.width > container.width) cdim.width = container.width;
		if (cdim.height > container.height) cdim.height = container.height;
		return cdim;
	}

	/*******************************************************************************************************************
	 * Рассчитывает максимальные пропорциональные размеры компонента, при которых он помещается в контейнер. При
	 * необходимости ширина и высота изменяются пропорционально.
	 * 
	 * @param component размеры компонента
	 * @param container размеры контейнера
	 * @return масштабированные максимальные размеры компонента, не больше размеров контейнера
	 ******************************************************************************************************************/
	public static Dimension calcScaleMatchDimension(final Dimension component, final Dimension container) {
		int width;
		int height;
		final double widthRatio = (double) container.width / component.width;
		final double heightRatio = (double) container.height / component.height;
		if (widthRatio < heightRatio) {
			width = (int) (component.width * widthRatio);
			height = (int) (component.height * widthRatio);
		} else {
			width = (int) (component.width * heightRatio);
			height = (int) (component.height * heightRatio);
		}
		return new Dimension(width, height);
	}

	/*******************************************************************************************************************
	 * Расчитывает пропорциональные размеры компонента, при которых он помещается в контейнер. Если размеры компонента
	 * меньше размеров контейнера, то они остаются без изменения.
	 * 
	 * @param component размеры компонента
	 * @param container размеры контейнера
	 * @return масштабированные размеры компонента, не больше размеров контейнера
	 ******************************************************************************************************************/
	public static Dimension calcScaleFitDimension(final Dimension component, final Dimension container) {
		Dimension dim = null;
		if (component.width > container.width || component.height > container.height) dim = GuiMisc.calcScaleMatchDimension(
		        component, container);
		else dim = component;
		return dim;
	}

	/*******************************************************************************************************************
	 * Рассчитывает относительное координаты компонента по центру контейнера. Размер компонента не изменяется, даже если
	 * он больше контейнера.
	 * 
	 * @param component размеры центрируемого компонента
	 * @param container размеры контейнера
	 * @return координаты компонента относительно контейнера.
	 ******************************************************************************************************************/
	public static Point calcRelativeCenter(final Dimension component, final Dimension container) {
		final int x = (container.width - component.width) / 2;
		final int y = (container.height - component.height) / 2;
		return new Point(x, y);
	}

	/*******************************************************************************************************************
	 * Рассчитывает абсолютные координаты компонента по центру контейнера. Размеры компонента не изменяются даже если он
	 * меньше контейнера.
	 * 
	 * @param component
	 * @component размеры компонента
	 * @param container границы контейнера
	 * @return абсолютные координаты компонента по центру контейнера
	 ******************************************************************************************************************/
	public static Point calcAbsoluteCenter(final Dimension component, final Rectangle container) {
		final Point p = GuiMisc.calcRelativeCenter(component, container.getSize());
		p.x += container.x;
		p.y += container.y;
		return p;
	}

	/*******************************************************************************************************************
	 * Центрирует компонент по центру экрана. Если компонент больше экрана, то его размеры урезаются по размерам экрана.
	 * 
	 * @param component
	 ******************************************************************************************************************/
	public static void centerComponentToScreen(final Component component) {
		final Dimension ss = Toolkit.getDefaultToolkit().getScreenSize();
		Dimension cs = component.getSize();
		cs = GuiMisc.calcCropFitDimension(cs, ss);
		component.setSize(cs);
		final Point cl = GuiMisc.calcRelativeCenter(cs, ss);
		component.setLocation(cl);
	}

	/*******************************************************************************************************************
	 * Проверяет принадлежность компонента контейнеру
	 * 
	 * @param container
	 * @param component
	 * @return component
	 ******************************************************************************************************************/
	public static boolean containComponent(final Container container, final Component component) {
		final Component[] components = container.getComponents();
		for (final Component element : components) {
			if (element == component) return true;
		}
		return false;
	}

	/*******************************************************************************************************************
	 * Возвращает Frame или Dialig компонента
	 * 
	 * @param component
	 * @return window
	 ******************************************************************************************************************/
	public static Window getOwnerWindow(final Component component) {
		Component owner = component;
		if (owner == null) owner = JOptionPane.getRootFrame();
		else if (!(owner instanceof Frame || owner instanceof Dialog)) owner = GuiMisc.getOwnerWindow(owner.getParent());
		return (Window) owner;
	}

	/**
	 * Scales down a dimension, component, so that it fits inside another dimension, container, while preserving the
	 * aspect ratio of component.
	 * 
	 * @param container Containing dimension
	 * @param component Contained dimension
	 * @return the largest dimension that has the aspect ratio of component and is no larger than either component or
	 *         container
	 */
	public static final Dimension scaleDown(final Dimension container, final Dimension component) {
		final Dimension scaled = GuiMisc.calcScaleFitDimension(component, container);
		return scaled.width > component.width || scaled.height > component.height ? component : scaled;
	}

	/**
	 * Converts a string to an integer.
	 * 
	 * @param string integer represented as a string
	 * @return If <code>string</code> represents a valid number, the number represented. Otherwise, 0.
	 */
	public static final int integer(final String string) {
		int value = 0;
		try {
			value = new Integer(string).intValue();
		} catch (final NumberFormatException error) {
			// NOP
		}
		return value;
	}

	/**
	 * Creates an image of the given size. Unlike <code>component.createImage
	 * </code>, this method will not return null.
	 * 
	 * @param dimension size of the image
	 * @return a <code>dimension.width</code> by <code>dimension.height</code> image
	 */
	public static final Image createImage(final Dimension dimension) {
		return GuiMisc.createImage(dimension.width, dimension.height);
	}

	/**
	 * Creates an image of the given width and height. Unlike <code>
	 * component.createImage</code>, this method will not return
	 * null.
	 * 
	 * @param swidth width of the image
	 * @param sheight height of the image
	 * @return a <code>width</code> by <code>height</code> image
	 */
	public static final Image createImage(final int swidth, final int sheight) {
		int width = swidth;
		int height = sheight;
		// Check parameters
		if (width < 1) width = 1;
		if (height < 1) height = 1;
		// Make sure frame exists
		if (GuiMisc.dummyWindow == null) {
			GuiMisc.dummyFrame = new Frame();
			GuiMisc.dummyWindow = new Window(GuiMisc.dummyFrame);
			GuiMisc.dummyWindow.setSize(0, 0);
			GuiMisc.dummyWindow.setVisible(true);
		}
		// Use the window to create the image (since it has a peer)
		return GuiMisc.dummyWindow.createImage(width, height);
	}

	/**
	 * Gets the topmost container of a given component.
	 * 
	 * @param component component being examined
	 * @return a container that directly or indirectly contains <code>component
	 *         </code>, but is not contained by anything, or
	 *         null is component does not have a parent (container)
	 */
	public static final Container getTopmostContainer(final Component component) {
		// Make sure component is not null
		if (component == null) return null;
		Container next = component.getParent();
		// Continue up the ancestor chain
		while (next != null) {
			next = next.getParent();
		}
		return next;
	}

	/**
	 * Packs an array into it's most condense form.
	 * 
	 * @param array any array of objects
	 * @param defaultClass
	 * @return If <code>array</code> is null, an array of zero objects. If <code>array</code> is not null, an array of n
	 *         objects where n is the number of non-null elements in <code>array</code>.
	 *         <p>
	 *         The return value may be typed casted into the class of <code>
	 *         array</code> as long as <code>array</code>
	 *         was not null. If <code>
	 *         array</code> is null, it may be typed casted into an array of
	 *         <code>defaultClass</code>. If both <code>array</code> and <code>
	 *         defaultClass</code> are null, the
	 *         return value will be an array of class Object.
	 *         </p>
	 *         <p>
	 *         This method will never return null.
	 *         </p>
	 */
	public static final Object[] packArray(final Object array[], final Class<?> defaultClass) {
		try {
			// Check for null array
			if (array == null) return defaultClass == null ? new Object[0] : (Object[]) Array.newInstance(defaultClass,
			        0);
			// Get the number of non-null elements in array
			int numElement = 0;
			for (final Object element : array)
				if (element != null) numElement++;
			// Get the array's class
			final Class<?> c = array.getClass().getComponentType();
			// Create an array that has the same type as the array parameter
			final Object result[] = (Object[]) Array.newInstance(c, numElement);
			// Populate the return code array
			numElement = 0;
			for (final Object element : array)
				if (element != null) result[numElement++] = element;
			return result;
		} catch (final NegativeArraySizeException e) {
			// This cannot happen since numElement >= 0
			return null;
		}
	}

	/**
	 * Gets a method.
	 * 
	 * @param className fully qualified name of class containing method, e.g., "myPackage.subPackage.myClass"
	 * @param methodName name of the method that compares two objects
	 * @param paramType the fully qualified names of the classes of every parameter in the parameter order.
	 * @return On success, an instance of Method that may be used to invoke the specified method. On failure, null. This
	 *         method will only fail if the given method cannot be found or is not accessible. On failure, this method
	 *         will print a stack trace of the exception encountered.
	 */
	public static final Method getMethod(final String className, final String methodName, final String paramType[]) {
		final String[] _paramType = paramType != null ? paramType : new String[0];
		try {
			// Get class class
			final Class<?> aClass = Class.forName(className);
			// Get each parameter
			final Class<?> parameter[] = new Class[_paramType.length];
			for (int counter = 0; counter < parameter.length; counter++) {
				parameter[counter] = Class.forName(_paramType[counter]);
			}
			// Get method
			final Method method = aClass.getMethod(methodName, parameter);
			return method;
		} catch (final Exception error) {
			error.printStackTrace();
			return null;
		}
	}

	/**
	 * Gets the name of the class and the method that called this method.
	 * 
	 * @param level number of stack entries to skip. If the caller wants to know who it is, <code>level</code> should be
	 *            1. If the caller wants to know who called it, <code>level</code> should be 2. Etc. If
	 *            <code>level</code> is 0 or less, this class and method is returned. If <code>level</code> is too high,
	 *            empty strings are returned in a non-null array.
	 * @return an array, s, of three strings such that s[0] is the class name, s[1] is the method name, and s[2] is the
	 *         line number. Any, some, or all of these strings can be "", if the information could not be determined.
	 *         The array s, itself, will not be null. s.length will always be 3.
	 */
	public static final String[] getClassAndMethod(final int level) {
		// Mark className and methodName as unknown
		String className = "";
		String methodName = "";
		String lineNumber = "";
		try {
			throw new Exception();
		} catch (final Exception exception) {
			try {
				// Put stack trace into an array of bytes. The array only has to
				// be
				// large enough to store the top four lines of text. The byte
				// array
				// will grow to accommodate the stack trace, but it's more
				// effecient
				// to initially allocate enough space for most stack traces.
				final ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
				final PrintWriter pw = new PrintWriter(os);
				exception.printStackTrace(pw);
				pw.flush();
				final byte array[] = os.toByteArray();
				// Go to start of (level) line. The first line says stack trace.
				// Then there may be several lines before this method.
				int indexLineStart = 0;
				for (int i = 0; i < level + GuiMisc.levelOffset; i++) {
					while (array[indexLineStart] != '\n')
						indexLineStart++;
					indexLineStart++;
				}
				// Go to start of class name (after "at ")
				int indexClassStart = indexLineStart;
				while (array[indexClassStart] == ' ')
					indexClassStart++;
				while (array[indexClassStart] != ' ')
					indexClassStart++;
				indexClassStart++;
				// Go to end of the method name (right before " " or "(")
				int indexMethodEnd = indexClassStart;
				while (array[indexMethodEnd] != ' ' && array[indexMethodEnd] != '(')
					indexMethodEnd++;
				// Go to start of the method name (right after ".")
				int indexMethodStart = indexMethodEnd;
				while (array[indexMethodStart] != '.')
					indexMethodStart--;
				indexMethodStart++;
				// Go to end of class name (two characters left of method start)
				final int indexClassEnd = indexMethodStart - 1;
				// Go to end of line number (two characters left of newline if
				// available in stack trace)
				int indexNumEnd = indexMethodEnd;
				while (array[indexNumEnd] != '\n')
					indexNumEnd++;
				indexNumEnd -= 2;
				// Go to start of line number (right after ':')
				int indexNumStart = indexNumEnd;
				while (indexNumStart > indexMethodEnd && array[indexNumStart] != ':')
					indexNumStart--;
				if (indexNumStart == indexMethodEnd) indexNumStart = indexNumEnd;
				else indexNumStart++;
				// Extract class name, method name, and line number
				className = new String(array, indexClassStart, indexClassEnd - indexClassStart);
				methodName = new String(array, indexMethodStart, indexMethodEnd - indexMethodStart);
				lineNumber = new String(array, indexNumStart, indexNumEnd - indexNumStart);
			} catch (final Exception error) {
				// Stack trace was too big or not properly formatted. In either
				// case, both the class and the method names are unknown.
			}
		}
		// Package strings into a single array
		final String array[] = { className, methodName, lineNumber };
		return array;
	}

	/**
	 * Calculates the number of lines to skip to find the top level of the stack trace.
	 */
	protected static final int calculateLevelOffset() {
		@SuppressWarnings("hiding")
		int levelOffset = 0;
		try {
			throw new Exception();
		} catch (final Exception exception) {
			try {
				// Put stack trace into an array of bytes. The array only has to
				// be
				// large enough to store the top four lines of text. The byte
				// array
				// will grow to accommodate the stack trace, but it's more
				// effecient
				// to initially allocate enough space for most stack traces.
				final ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
				final PrintWriter pw = new PrintWriter(os);
				exception.printStackTrace(pw);
				pw.flush();
				final byte array[] = os.toByteArray();
				// Get the name of this class
				final String nameStr = (new GuiMisc()).getClass().getName();
				final byte name[] = nameStr.getBytes();
				// Find offset to classname in terms of lines of text
				int i = 0;
				boolean found = false;
				while (!found) {
					// Find match for class name
					int c;
					for (c = 0; c < name.length; c++)
						if (name[c] != array[i + c]) break;
					// Was a match found
					if (c == name.length) found = true;
					else {
						// Increment line count if a newline was reached
						if (array[i] == '\n') levelOffset++;
						i++;
					}
				}
			} catch (final Exception error) {
				// Stack trace was too big or not properly formatted. In either
				// case, the only thing to do is hope that the levelOffset is 0
				levelOffset = 0;
			}
		}
		return levelOffset;
	}

	/*******************************************************************************************************************
	 * Возвращает обьект типа BufferedImage, соответствующий заданному обьекту Image
	 * 
	 * @param image
	 * @return image
	 ******************************************************************************************************************/
	public static BufferedImage makeBufferedImage(final Image image) {
		if (image != null) {
			final int imageWidth = image.getWidth(null);
			final int imageHeight = image.getHeight(null);
			final BufferedImage bufferedImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_BGR);
			final Graphics graphics = bufferedImage.getGraphics();
			graphics.drawImage(image, 0, 0, imageWidth, imageHeight, null);
			return bufferedImage;
		}
		return null;
	}
}
